package com.atguigu.tingshu.account.service.impl;

import com.alibaba.cloud.commons.lang.StringUtils;
import com.alibaba.fastjson.JSON;
import com.atguigu.tingshu.account.mapper.RechargeInfoMapper;
import com.atguigu.tingshu.account.mapper.UserAccountDetailMapper;
import com.atguigu.tingshu.account.mapper.UserAccountMapper;
import com.atguigu.tingshu.account.service.UserAccountService;
import com.atguigu.tingshu.common.constant.SystemConstant;
import com.atguigu.tingshu.common.execption.GuiguException;
import com.atguigu.tingshu.common.result.ResultCodeEnum;
import com.atguigu.tingshu.common.util.AuthContextHolder;
import com.atguigu.tingshu.model.account.RechargeInfo;
import com.atguigu.tingshu.model.account.UserAccount;
import com.atguigu.tingshu.model.account.UserAccountDetail;
import com.atguigu.tingshu.model.payment.PaymentInfo;
import com.atguigu.tingshu.vo.account.AccountLockResultVo;
import com.atguigu.tingshu.vo.account.AccountLockVo;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.math.BigDecimal;
import java.util.concurrent.TimeUnit;

@Slf4j
@Service
@SuppressWarnings({"unchecked", "rawtypes"})
public class UserAccountServiceImpl extends ServiceImpl<UserAccountMapper, UserAccount> implements UserAccountService {

	@Autowired
	private UserAccountMapper userAccountMapper;
	@Autowired
	private UserAccountDetailMapper userAccountDetailMapper;
	@Autowired
	private StringRedisTemplate redisTemplate;
	@Autowired
	private RechargeInfoMapper rechargeInfoMapper;

	@Override
	public BigDecimal getAvailableAmount() {
		// 根据用户 Id 获取可用余额
		UserAccount userAccount = userAccountMapper.selectOne(new LambdaQueryWrapper<UserAccount>()
				.eq(UserAccount::getUserId, AuthContextHolder.getUserId()));
		return userAccount.getAvailableAmount();
	}

	@Transactional
	@Override
	public AccountLockResultVo checkAndLock(AccountLockVo accountLockVo) {
		String orderNo = accountLockVo.getOrderNo();
		Long userId = accountLockVo.getUserId();
		BigDecimal amount = accountLockVo.getAmount();
		// 构建防止重复请求的redisKey，保证接口幂等性
		String idempotentKey = "check:idempotent:" + orderNo;
		// 构建检查锁定结果的数据key
		String accountLockResultKey = "account:lock:result:" + orderNo;
		// 检查是否是重复请求
		// 第一次将 AccountLockResultVo 存入 redis
		// 向 redis 中 使用 setNx 方法存数据,如果该 Key 存在,返回 false,不存在就存入并返回 true
		Boolean ifAbsent = redisTemplate.opsForValue().setIfAbsent(idempotentKey, orderNo, 1, TimeUnit.HOURS);
		// 如果是返回值是 false 就是重复请求
		if (!ifAbsent) {
			// 重复请求就从 redis 里面拿数据并返回,没有数据就报错
			String accountLockResultVoJSON = redisTemplate.opsForValue().get(accountLockResultKey);
			if (!StringUtils.isEmpty(accountLockResultVoJSON)) {
				return JSON.parseObject(accountLockResultVoJSON, AccountLockResultVo.class);
			} else { // 还未计算出结果就再次请求
				throw new GuiguException(ResultCodeEnum.ACCOUNT_LOCK_REPEAT);
			}
		}


		// 获取当前账户表中 该UserId下面的可用金额大于传入金额的账户
		// 使用 for update,其中如果条件是主键,索引,范围主键,范围索引(in (id1,id2...))就是行锁,使用普通列就是表锁
		// 使用 for update,必须开启事务
		UserAccount userAccount = userAccountMapper.selectOne(new LambdaQueryWrapper<UserAccount>()
				.eq(UserAccount::getUserId, userId)
				.ge(UserAccount::getAvailableAmount, amount)
				.last(" for update"));
		// 如果这个账户不存在,就说明可用余额不够
		if (null == userAccount) {
			throw new GuiguException(ResultCodeEnum.ACCOUNT_LESS);
		}
		// 尝试锁定金额
		int lock = userAccountMapper.lock(userId, amount);
		// 如果是返回0,那么就是上锁失败了,抛异常
		if (lock == 0) {
			throw new GuiguException(ResultCodeEnum.ACCOUNT_LOCK_ERROR);
		}
		// 添加账户的变更的日志
		this.log(accountLockVo.getUserId(), "锁定：" + accountLockVo.getContent(),
				SystemConstant.ACCOUNT_TRADE_TYPE_LOCK, accountLockVo.getAmount(),
				"lock:" + accountLockVo.getOrderNo());
		// 构建返回对象
		AccountLockResultVo accountLockResultVo = new AccountLockResultVo();
		accountLockResultVo.setUserId(userId);
		accountLockResultVo.setAmount(amount);
		accountLockResultVo.setContent(accountLockVo.getContent());
		// 将构建好的对象存入 redis
		redisTemplate.opsForValue().set(accountLockResultKey, JSON.toJSONString(accountLockResultVo), 1, TimeUnit.HOURS);
		return accountLockResultVo;
	}

	@Override
	public void minus(String orderNo) {
		// 定义保证幂等性的key
		String idempotentKey = "account:minus:idempotent:" + orderNo;

		// 数据设置失败，说明是重复提交
		Boolean isExist = redisTemplate.opsForValue().setIfAbsent(idempotentKey, orderNo, 1, TimeUnit.HOURS);
		if (Boolean.FALSE.equals(isExist)) return;

		// 锁定账号结果信息key
		String accountLockResultKey = "account:lock:result:" + orderNo;
		String accountLockResultJSON = this.redisTemplate.opsForValue().get(accountLockResultKey);
		if (StringUtils.isEmpty(accountLockResultJSON)) return;
		// 扣减账户金额
		AccountLockResultVo accountLockResultVo = JSON.parseObject(accountLockResultJSON, AccountLockResultVo.class);

		int minus = userAccountMapper.minus(accountLockResultVo.getUserId(), accountLockResultVo.getAmount());
		if (minus == 0) {
			redisTemplate.delete(idempotentKey);   // 删除幂等性保证的key
			throw new GuiguException(ResultCodeEnum.ACCOUNT_MINUSLOCK_ERROR);
		}
		// 记录日志
		this.log(accountLockResultVo.getUserId(),
				accountLockResultVo.getContent(),
				SystemConstant.ACCOUNT_TRADE_TYPE_MINUS,
				accountLockResultVo.getAmount(), orderNo);

		// 解锁账户金额之后，删除锁定缓存
		this.redisTemplate.delete(accountLockResultKey);
	}

	@Override
	public void unlock(String orderNo) {
		// 定义保证幂等性的key
		String idempotentKey = "account:unlock:idempotent:" + orderNo;

		// 数据设置失败，说明是重复提交
		Boolean isExist = redisTemplate.opsForValue().setIfAbsent(idempotentKey, orderNo, 1, TimeUnit.HOURS);
		if (!isExist) return;

		// 锁定账号结果信息key
		String accountLockResultKey = "account:lock:result:" + orderNo;
		String accountLockResultJSON = this.redisTemplate.opsForValue().get(accountLockResultKey);
		if (StringUtils.isEmpty(accountLockResultJSON)) return;

		// 扣减账户金额
		AccountLockResultVo accountLockResultVo = JSON.parseObject(accountLockResultJSON, AccountLockResultVo.class);

		// 调用解锁方法
		int unLock = userAccountMapper.unLock(accountLockResultVo.getUserId(), accountLockResultVo.getAmount());
		if (unLock == 0) {
			redisTemplate.delete(idempotentKey);   // 删除幂等性保证的key
			throw new GuiguException(ResultCodeEnum.ACCOUNT_UNLOCK_ERROR);
		}

		// 记录日志
		this.log(accountLockResultVo.getUserId(), "解锁：" + accountLockResultVo.getContent()
				, SystemConstant.ACCOUNT_TRADE_TYPE_UNLOCK, accountLockResultVo.getAmount(),
				"unlock:" + orderNo);

		// 解锁账户金额之后，删除锁定缓存
		this.redisTemplate.delete(accountLockResultKey);
	}

	@Override
	public void updateRechargeStatus(String orderNo) {
		RechargeInfo rechargeInfo = rechargeInfoMapper.selectOne(new LambdaQueryWrapper<RechargeInfo>().eq(RechargeInfo::getOrderNo, orderNo));
		// 如果是已支付状态就不更改
		if (SystemConstant.ORDER_STATUS_PAID.equals(rechargeInfo.getRechargeStatus())) return;
		rechargeInfo.setRechargeStatus(SystemConstant.ORDER_STATUS_PAID);
		rechargeInfoMapper.updateById(rechargeInfo);
		// 	更改账户金额
		Long userId = rechargeInfo.getUserId();
		UserAccount userAccount = userAccountMapper.selectOne(new LambdaQueryWrapper<UserAccount>().eq(UserAccount::getUserId, userId));
		userAccount.setTotalAmount(userAccount.getTotalAmount().add(rechargeInfo.getRechargeAmount()));
		userAccount.setAvailableAmount(userAccount.getAvailableAmount().add(rechargeInfo.getRechargeAmount()));
		userAccountMapper.updateById(userAccount);
		// 记录日志
		this.log(userId, "充值", SystemConstant.ACCOUNT_TRADE_TYPE_DEPOSIT, rechargeInfo.getRechargeAmount(), orderNo);
	}

	@Override
	public IPage<UserAccountDetail> findUserRechargePage(Integer pageNo, Integer pageSize) {
		return this.findTradePageByType(pageNo,pageSize,SystemConstant.ACCOUNT_TRADE_TYPE_DEPOSIT);
	}
	@Override
	public IPage<UserAccountDetail> findUserConsumePage(Integer pageNo, Integer pageSize) {
		return this.findTradePageByType(pageNo,pageSize,SystemConstant.ACCOUNT_TRADE_TYPE_MINUS);
	}

	private IPage<UserAccountDetail> findTradePageByType(Integer pageNo,Integer pageSize,String tradeType){Long userId = AuthContextHolder.getUserId();
		IPage<UserAccountDetail> page = new Page<>(pageNo, pageSize);
		page = userAccountDetailMapper.selectPage(page, new LambdaQueryWrapper<UserAccountDetail>()
				.eq(UserAccountDetail::getUserId, userId)
				.eq(UserAccountDetail::getTradeType, tradeType)
				.orderByDesc(UserAccountDetail::getUpdateTime));
		return page;
	}


	/**
	 * 保存账户变更日志数据
	 *
	 * @param userId    用户Id
	 * @param title     标题
	 * @param tradeType 订单类型
	 * @param amount    金额
	 * @param orderNo   订单编号
	 */
	private void log(Long userId, String title, String tradeType, BigDecimal amount, String orderNo) {
		// 构建账户明细日志对象
		UserAccountDetail userAccountDetail = new UserAccountDetail();
		userAccountDetail.setUserId(userId);
		userAccountDetail.setTitle(title);
		userAccountDetail.setTradeType(tradeType);
		userAccountDetail.setAmount(amount);
		userAccountDetail.setOrderNo(orderNo);
		// 保存数据
		userAccountDetailMapper.insert(userAccountDetail);
	}
}
